并发编程(1) | 您所在的位置:网站首页 › cpu 编程 › 并发编程(1) |
原子性高频问题
1.java中如何实现线程安全?
多线程操作共享数据出现的问题。 锁: 悲观锁:synchronized,lock(AQS实现) 乐观锁:CAS 可以根据业务情况,选择ThreadLocal。 2.CAS底层实现java角度:先比较值是否一致,一致则交换,返回true;否则,返回false native调用了本地依赖库中的C++中的方法——unsafe方法 在CAS底层,如果是多核系统,增加了一个lock指令,这里使用了C++的内联汇编 汇编指令cmpxchg, CPU硬件底层就支持 比较和交换 (cmpxchg),cmpxchg并不保证原子性的。(cmpxchg的操作是不能再拆分的指令)所以才会出现判断CPU是否是多核,如果是多核就追加lock指令。lock指令可以理解为是CPU层面的锁,一般锁的粒度就是 缓存行 级别的锁,当然也有 总线锁 ,但是成本太高,CPU会根据情况选择。 3.CAS的ABA问题此类问题常出现在++,- -的操作中。 线程A:期望将value从A1 - B2 线程B:期望将value从B2 - A3 线程C:期望将value从A1 - C4 从原子性来考虑,是无法保证线程安全的,如何解决此类问题呢?在java的JUC下AtomicStampedReference中提供了解决方案,即在修改value的同时,指定好版本号。 可见性问题 1.Java的内存模型JMM,不是JVM内存模型!!! 缓存是CPU的缓存,CPU的缓存分为L1(线程独享),L2(内核独享),L3(多核共享) JMM就是Java内存模型的核心,可见性,有序性都基于这实现。 主内存JVM,就是你堆内存。 在处理指令时,CPU会拉取数据,优先级是从L1到L2到L3,如果都没有,需要去主内存中拉取,JMM就是在CPU和主内存之间,来协调,保证可见、有序性等操作。 2.保证可见性的方式可见性是指线程间的,对变量的变化是否可见 Java层面: volatile,用volatile基本数据类型,可以保证每次CPU去操作数据时,都直接去主内存进行读写。 synchronized,synchronized的内存语义可以保证在获取锁之后,可以保证前面操作的数据是可见的。 lock(CAS-volatile),也可以保证CAS或者操作volatile的变量之后,可以保证前面操作的数据是可见的。 final,是常量没法动~~ 3.有了MESI,为什么还有volatile?MESI是CPU缓存一致性的协议,大多数的CPU厂商都根据MESI去实现了缓存一致性的效果。 CPU已经有MESI协议了,volatile是不是有点多余啊!? 首先,这俩不冲突,一个是从CPU硬件层面上的一致性,一个是Java中JMM层面的一致性。 MESI协议,有一套固定的机制,无论是否声明了volatile,都会基于这个机制来保证缓存的一致性(可见性)。同时,也要清楚,如果没有MESI协议,volatile也会存在一些问题,不过也有其他的处理方案(总线锁,时间成本太高了,如果锁了总线,就一个CPU核心在干活)。 MESI是协议,是规划,是interface,他需要CPU厂商实现。 既然CPU有MESI了,为啥还要volatile,那自然是MESI协议有问题。MESI保证了多核CPU的独占cache之间的可见性,但是CPU不是说必须直接将寄存器中的数据写入到L1,因为在大多是×86架构的CPU中,寄存器和L1之间有一个store buffer,寄存器值可能落到了store buffer,没落到L1中,就会导致缓存不一致。而且除了×86架构的CPU,在arm和power的CPU中,还有load buffer,invalid queue都会或多或少影响缓存一致性! 有序性 1.什么是有序性单例模式中的懒汉机制中,就存在一个这样的问题。 懒汉为了保证线程安全,一般会采用DCL的方式。 但是单单用DCL,依然会有几率出现问题。 线程可能会拿到初始化一半的对象去操作,极有可能出现NullPointException。 (初始化对象三部,开辟空间,初始化内部属性,指针指向引用) 在Java编译.java为.class时,会基于JIT做优化,将指令的顺序做调整,从而提升执行效率。 在CPU层面,也会对一些执行进行重新排序,从而提升执行效率。 |
CopyRight 2018-2019 实验室设备网 版权所有 |